import asyncio
import os
import time

import math
import subprocess
from datetime import datetime
import gc
import serial
import serial_asyncio
import asyncio
from pynmeagps import NMEAReader

from common.third_party.devmem import DevMemCache
from common.third_party.save_config import load_const, save_config, load_config
from common.third_party.sfp import sfp_init, sfp_deinit, sfp_read_rx_power_dbm, sfp_read_tx_power_watt
from services.control.api import ControlApi
from services.server_integration.api import ServerIntegrationApi
from settings import settings

from .schemas.dsp_telemetry import DspTelemetry, DspTelemetryUartInfo
from .schemas.gps_telemetry import GpsTelemetry
from .schemas.pwr_telemetry import PwrTelemetry
from .schemas.sfp_telemetry import SfpTelemetry
from common.third_party.log import new_logger
import logging

class Telemetry:
    __slots__ = ['gps_telemetry', 'dsp_telemetry', 'pwr_telemetry', 'sfp_telemetry', 'mem', 'numSV', '_logger_gps']

    GPS_CONFIG: dict = {'port_name': '/dev/ttyUL1', 'baudrate': 115200, 'timeout': 1, 'stopbits': 2}
    PWR_CONFIG: dict = {'port_name': '/dev/ttyUL3', 'baudrate': 38400, 'timeout': 1, 'stopbits': 2}
    DSP_CONFIG: dict = {'port_name': '/dev/ttyUL2', 'baudrate': 38400, 'timeout': 1, 'stopbits': 2}

    CTR_API = ControlApi()
    SI_API = ServerIntegrationApi()

    def __init__(self) -> None:
        self._logger_gps = new_logger('tl-gps', level=logging.ERROR, stdout_handler=False)
        self.mem = DevMemCache()

        self.numSV = 0
        self.gps_telemetry = GpsTelemetry()
        self.dsp_telemetry = DspTelemetry()
        self.pwr_telemetry = PwrTelemetry()
        self.sfp_telemetry = SfpTelemetry()

        self.init_gps()

    async def sfp_telemetry_loop(self) -> None:
        if settings.DEBUG:
            return

        sfp_inited = [False, False]
        while True:
            for index in range(2):
                module = self.sfp_telemetry.modules[index]
                module.mod_abs = self.read_sfp_telemetry_value(index, f'mod_abs')
                module.rx_los = self.read_sfp_telemetry_value(index, 'rx_los')
                module.tx_fault = self.read_sfp_telemetry_value(index, 'tx_fault')

                match (module.mod_abs, sfp_inited[index]):
                    case (False, True):
                        module.rx_power_dbm = self.read_sfp_telemetry_value(index, 'rx_power_dbm')
                        module.tx_power_watt = self.read_sfp_telemetry_value(index, 'tx_power_watt') / 1000
                    case (True, False):
                        pass
                    case (False, False):
                        sfp_inited[index] = self.try_init_sfp(index)
                    case (True, True):
                        if not module.rx_los:
                            self.try_deinit_sfp(index)
                            sfp_inited[index] = False
                            module.rx_power_dbm = 0
                            module.tx_power_watt = 0

            await asyncio.sleep(2.0)

    async def run_telemetry_loops(self) -> None:
        if not settings.USE_OLD_PWR or await self.CTR_API.get_control_state('block_state'):
            pwr_futures = self.read_pwr_telemetry_loop()
        else:
            pwr_futures = self.read_old_pwr_telemetry_loop()

        await asyncio.gather(
            asyncio.create_task(self.read_gps_telemetry_loop()),
            asyncio.create_task(pwr_futures),
            asyncio.create_task(self.read_dsp_telemetry_loop()),
            asyncio.create_task(self.sfp_telemetry_loop()),
            asyncio.create_task(self.fix_telemetry_loop())
        )

    def run_in_eternal_loop(self, func, sleep):
        async def loop():
            while True:
                try:
                    await func()
                except Exception as e:
                    #print(e)
                    pass

                await asyncio.sleep(sleep)

        asyncio.create_task(loop())

    async def read_gps_collect_sub_loop(self):
        prev_gsv_data = {}
        writer = None
        for _ in range(33):
            try:
                target_gps_pos = await self.read_gps_position()
                reader, writer = await asyncio.wait_for(serial_asyncio.open_serial_connection(
                    loop=asyncio.get_event_loop(),
                    url=self.GPS_CONFIG.get('port_name'), baudrate=self.GPS_CONFIG.get('baudrate'),
                    timeout=1
                ), 3.0)
                gsv_data = {sv_count: {} for sv_count in range(32)}
                gps_telemetry = None
                max_num_sv = 0
                for _ in range(25):
                    gps_message = await asyncio.wait_for(reader.readline(), 1.2)
                    if gps_message is None:
                        continue

                    if b'RMC' in gps_message:
                        # http://aprs.gids.nl/nmea/#rmc
                        try:
                            rmc_data = NMEAReader.parse(gps_message)
                            timestamp = datetime(year=rmc_data.date.year, month=rmc_data.date.month,
                                                 day=rmc_data.date.day,
                                                 hour=rmc_data.time.hour, minute=rmc_data.time.minute,
                                                 second=rmc_data.time.second)

                            gps_telemetry = GpsTelemetry(status=rmc_data.status == 'A',
                                                         timestamp=timestamp, latitude=rmc_data.lat,
                                                         north_sauth=rmc_data.NS, longitude=rmc_data.lon,
                                                         east_west=rmc_data.EW, raw_message=str(gps_message))
                            break
                        except Exception as e:
                            # print(e)
                            continue
                    elif b'GSV' in gps_message:
                        try:
                            rmc_data = NMEAReader.parse(gps_message)
                            max_num_sv = max(max_num_sv, rmc_data.numSV)

                            for msg_sub_index in range(5):
                                snr = getattr(rmc_data, f'cno_0{msg_sub_index}', None)
                                sv_id = getattr(rmc_data, f'svid_0{msg_sub_index}', None)
                                if snr is not None and sv_id is not None:
                                    gsv_data[rmc_data.numSV][sv_id] = snr
                        except Exception as e:
                            # print(e)
                            continue

                for sv_count, sv_pack in gsv_data.items():
                    sv_pack = dict(filter(lambda x: isinstance(x[1], int), sv_pack.items()))
                    if len(sv_pack) > 0:
                        prev_pack = prev_gsv_data.get(sv_count, [])
                        if prev_pack != sv_pack:
                            prev_gsv_data[sv_count] = sv_pack
                            self._logger_gps.info(f'Count: {len(sv_pack)}({sv_count}), SNR: {sv_pack}')

                self.numSV = max_num_sv

                if gps_telemetry is not None:
                    if gps_telemetry.status:
                        gps_telemetry.status = self.validate_gps(self.gps_telemetry, target_gps_pos)

                    if gps_telemetry.status:
                        self._logger_gps.info(
                            f'time: {self.gps_telemetry.timestamp}, position: {self.gps_telemetry.latitude} {self.gps_telemetry.longitude} {self.gps_telemetry.north_south}{self.gps_telemetry.east_west}')
                        subprocess.call(
                            ['date', '-s', self.gps_telemetry.timestamp.strftime('%Y-%m-%d %H:%M:%S')],
                            stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
                        )
                        # subprocess.check_output(['date', timestmp, '+"%Y-%m-%d %k:%M:%S.%N"'])
                        # subprocess.check_output(['date', '-s', self.gps_telemetry.timestamp.strftime('%Y-%m-%d %H:%M:%S')])
                        save_config('tm/gps_set_time_time', self.gps_telemetry.timestamp.timestamp())
                        gps_telemetry.last_capture_time = self.gps_telemetry.timestamp
                    else:
                        last_capture_time = load_config('tm/gps_set_time_time', float, None)
                        if last_capture_time is not None:
                            gps_telemetry.last_capture_time = datetime.fromtimestamp(last_capture_time)

                    last_bs_capture_time = load_config('tm/last_bs_capture_time', float, None)
                    if last_bs_capture_time is not None:
                        gps_telemetry.last_bs_capture_time = datetime.fromtimestamp(last_bs_capture_time)

                    self.gps_telemetry = gps_telemetry

                try:
                    writer.transport.abort()
                except Exception as e:
                    pass
                try:
                    writer.transport.close()
                except Exception as e:
                    pass

                writer.close()
            except Exception as e:
                if writer is not None:
                    try:
                        writer.transport.abort()
                    except Exception as e:
                        pass
                    try:
                        writer.transport.close()
                    except Exception as e:
                        pass

                    writer.close()
                pass

            await asyncio.sleep(4.4)


    async def read_gps_telemetry_loop(self) -> None:
        while True:
            await asyncio.sleep(1)
            try:
                await self.read_gps_collect_sub_loop()
                for _ in range(4):
                    await asyncio.sleep(2)
                    gc.collect()
            except Exception as e:
                pass

    @staticmethod
    def decode_volt(v, k=2.5, s=4096):
        return v * k / s

    @staticmethod
    def decode_temp(value) -> float:
        #формула персчёта из даташита на микросхему
        return -2.3654 * value ** 2 - 78.154 * value + 153.857

    @staticmethod
    def volt_coef(u2, r1, r2) -> float:
        return (r1 + r2) * u2 / r2

    @staticmethod
    def amper_coef_28v_48v(u, c0=1.30271, c1=19.843) -> float:
        #c0 -- срединное напряжение реально измеренное перед делителем после микросхему
        #c1 -- коофицент в зависемости от типа микросхемы детектора
        return (u - c0) * c1

    @staticmethod
    def amper_coef_5v(u, c0=1.30271, c1=9.9216) -> float:
        return (u - c0) * c1

    async def fix_telemetry_loop(self):
        while True:
            await asyncio.sleep(60 * 15)
            try:
                for port in [self.GPS_CONFIG, self.PWR_CONFIG, self.DSP_CONFIG]:
                    ser = serial.Serial(port['port_name'], baudrate=port['baudrate'], timeout=1)
                    ser.reset_input_buffer()
                    ser.reset_output_buffer()
                    ser.send_break(2.0)
                    ser.close()
            except Exception as e:
                pass

    async def read_old_pwr_telemetry_loop(self):
        async def read():
            writer = None
            try:
                reader, writer = await asyncio.wait_for(serial_asyncio.open_serial_connection(
                    loop=asyncio.get_event_loop(),
                    url=self.PWR_CONFIG['port_name'], baudrate=self.PWR_CONFIG['baudrate'],
                    timeout=self.PWR_CONFIG['timeout']
                ), 3.0)
                pwr_message = b''
                for _ in range(200):
                    pwr_byte: bytes = await asyncio.wait_for(reader.read(n=1), 1)
                    if len(pwr_byte) > 0:
                        pwr_message += pwr_byte

                    if len(pwr_message) > 45 and b'ACDC_REV1.0' in pwr_message and b'END' in pwr_message:
                        pwr_message += await reader.read(n=2)
                        start_index = pwr_message.find(b'ACDC')
                        end_index = pwr_message.find(b'END') + len(b'END')
                        if start_index > end_index:
                            break

                        writer.close()

                        esrc = int(pwr_message[end_index:], 16)
                        msg = pwr_message[start_index: end_index]
                        src = sum(map(int, msg)) & 0xFF
                        if src != esrc:
                            return None
                        else:
                            return pwr_message[start_index: end_index]

                writer.close()
            except Exception as e:
                if writer is not None:
                    writer.close()

            return None

        while True:
            await asyncio.sleep(5.0)
            try:
                raw_data = await read()
                if raw_data is None:
                    continue

                block_type = await self.CTR_API.get_control_state('block_state')
                data = list(map(int, raw_data.split(b'#')[1:-1]))
                k = 3.22 if block_type else 2.048

                self.pwr_telemetry = PwrTelemetry(
                    current48v=self.amper_coef_28v_48v(self.decode_volt(data[0], k, 1024), 0.932, 9.99),
                    current28v=self.amper_coef_28v_48v(self.decode_volt(data[1], k, 1024), 1.311, 9.99),
                    current5v0=self.amper_coef_5v(self.decode_volt(data[3], k, 1024), 1.31, 19.9),
                    temperature5v=self.decode_temp(self.decode_volt(data[6], k, 1024)),
                    voltage28v=self.volt_coef(self.decode_volt(data[7], k, 1024), 160, 10),
                    voltage5v=self.volt_coef(self.decode_volt(data[8], k, 1024), 20, 10),
                    raw_message=str(raw_data)
                )
            except Exception as e:
                pass

    async def read_pwr_telemetry_loop(self) -> None:
        def parse(data: bytes):
            def crc(data: str):
                crc = 0xFFFF

                for ch in data:
                    crc ^= ord(ch)
                    for _ in range(0, 8):
                        bcarry = crc & 0x0001
                        crc >>= 1
                        if bcarry:
                            crc ^= 0xA001

                return crc

            data = data.decode('ascii')
            begin, *msgs, end = data.split('#')

            target_crc = int(end.removeprefix('END'), base=16)
            if begin not in ['D\x00CDC2.1', 'DCDC2.1']:
                return None

            msg = data.removeprefix(begin).removesuffix(end)
            msg = f'DCDC2.1{msg}END'
            if crc(msg) != target_crc:
                return None

            return [float(word) for word in msgs]

        while True:
            try:
                byte_array = await self._readline_serial(self.PWR_CONFIG['port_name'], self.PWR_CONFIG['baudrate'])
                if byte_array is None:
                    continue

                data = parse(byte_array)
                self.pwr_telemetry = PwrTelemetry(
                    current48v=self.amper_coef_28v_48v(self.decode_volt(data[0])),
                    current28v=self.amper_coef_28v_48v(self.decode_volt(data[1])),
                    current5v0=self.amper_coef_5v(self.decode_volt(data[2])),
                    voltage28v=self.volt_coef(self.decode_volt(data[3]), 110, 10),
                    voltage5v=self.volt_coef(self.decode_volt(data[4]), 15, 10),
                    temperature5v=self.decode_temp(self.decode_volt(data[5])),
                    raw_message=str(byte_array)
                )
            except Exception as e:
                #print(e)
                pass

            await asyncio.sleep(4.4)

    async def read_dsp_telemetry_loop(self) -> None:
        while True:
            writer = None
            try:
                reader, writer = await asyncio.wait_for(serial_asyncio.open_serial_connection(
                    loop=asyncio.get_event_loop(),
                    url=self.DSP_CONFIG['port_name'], baudrate=self.DSP_CONFIG['baudrate'],
                    timeout=self.DSP_CONFIG['timeout']
                ), 3.0)
                dsp_message = b''
                for _ in range(200):
                    dsp_byte: bytes = await asyncio.wait_for(reader.read(n=1), 1)
                    if len(dsp_byte) > 0:
                        dsp_message += dsp_byte
                    if b'SP_REV1.0#' in dsp_message and b'#END' in dsp_message:
                        break
                else:
                    self.dsp_telemetry = DspTelemetry()

                start_index: int = dsp_message.find(b'SP_REV1.0#') + len(b'SP_REV1.0#')
                end_index: int = dsp_message.find(b'#END')
                if start_index < end_index:
                    raw_dsp_array = list(map(int, dsp_message[start_index: end_index].decode('utf-8').split('#')))
                    dsp_telemetry_dict = DspTelemetry().dict()
                    dsp_telemetry_uart_info_dict = DspTelemetryUartInfo().dict()

                    for key in (dsp_telemetry_dict.keys() & dsp_telemetry_uart_info_dict.keys()):
                        info = dsp_telemetry_uart_info_dict[key]

                        if 'temperature' in key:
                            dsp_telemetry_dict[key] = self.calculate_temperature(
                                raw_dsp_array[info['index']],
                                info['coefficient']
                            )
                        else:
                            dsp_telemetry_dict[key] = self.calculate_voltage(
                                raw_dsp_array[info['index']],
                                info['coefficient']
                            )

                    self.dsp_telemetry = DspTelemetry(**dsp_telemetry_dict)
                    self.dsp_telemetry.raw_message = str(dsp_message)

                try:
                    writer.transport.abort()
                except Exception as e:
                    pass
                try:
                    writer.transport.close()
                except Exception as e:
                    pass

                writer.close()
            except Exception as e:
                if writer is not None:
                    try:
                        writer.transport.abort()
                    except Exception as e:
                        pass
                    try:
                        writer.transport.close()
                    except Exception as e:
                        pass

                    writer.close()

                self.dsp_telemetry = DspTelemetry()
                #print(e)
                pass

            await asyncio.sleep(4.4)

    async def _readline_serial(self, port_name: str, baudrate: int, timeout: int=1):
        writer = None
        try:
            reader, writer = await asyncio.wait_for(serial_asyncio.open_serial_connection(
                loop=asyncio.get_event_loop(),
                url=port_name, baudrate=baudrate,
                timeout=timeout
            ), 3.0)

            byte_array = await asyncio.wait_for(reader.readline(), 3.0)
            if byte_array is None:
                return None

            try:
                writer.transport.abort()
            except Exception as e:
                pass
            try:
                writer.transport.close()
            except Exception as e:
                pass
            try:
                writer.close()
            except Exception as e:
                pass

            return byte_array

        except Exception as e:
            if writer is not None:
                try:
                    writer.transport.abort()
                except Exception as e:
                    pass
                try:
                    writer.transport.close()
                except Exception as e:
                    pass
                try:
                    writer.close()
                except Exception as e:
                    pass

            #print(f'readline_serial: {port_name} -> {e}')
            return None

    def read_sfp_telemetry_value(self, index: int, name: str) -> bool | int:
        mem = self.mem.get(0x83DF0000)
        offset = index * 3
        match name:
            case 'rx_los':
                return mem.read(0)[offset + 0]
            case 'tx_fault':
                return mem.read(0)[offset + 1]
            case 'mod_abs':
                return mem.read(0)[offset + 2]
            case 'rx_power_dbm':
                return sfp_read_rx_power_dbm(index)
            case 'tx_power_watt':
                return sfp_read_tx_power_watt(index)

    @staticmethod
    def try_init_sfp(index: int):
        try:
            #print(f'sfp module {index} try init')
            sfp_init(index)
            return True
        except BaseException:
            return False

    @staticmethod
    def try_deinit_sfp(index: int) -> bool:
        try:
            sfp_deinit(index)
            return True
        except BaseException:
            return False

    @staticmethod
    def calculate_voltage(raw: float, coefficient: float = 2.0) -> float:
        voltage = raw * coefficient * (3.3 / 1024)
        return voltage

    @staticmethod
    def calculate_temperature(raw: float, coefficient: float = 1.0) -> float:
        voltage = raw * coefficient * (3.3 / 1024)
        temperature = 2.3654 * voltage ** 2 - 78.154 * voltage + 153.857
        return temperature

    @staticmethod
    def init_sfp():
        is_not_inited = True
        while is_not_inited:
            try:
                sfp_init(0)
                is_not_inited = False
            except OSError as e:
                #print(e)
                pass

    def init_gps(self):
        def encode_command(cmd: str):
            chars = [int(ch) for ch in cmd.encode('ascii')]
            ctc = 0x00
            for ch in chars:
                ctc ^= ch

            return f'${cmd}*{ctc:x}\r\n'.encode('ascii')

        with serial.Serial(
                port=self.GPS_CONFIG['port_name'], baudrate=self.GPS_CONFIG['baudrate'],
                timeout=self.GPS_CONFIG['timeout'], stopbits=self.GPS_CONFIG['stopbits']
        ) as gps_serial:
            gps_serial.send_break()
            gps_serial.flushOutput()
            gps_serial.flushInput()
            gps_serial.write(encode_command('PSIMPPS,W,3,999,0'))
            gps_serial.write(encode_command('PMTK285,3,100')) # 2d 3g фиксация, 100мс импульс
            # gps_serial.write(encode_command('PMTK353,0,0,0,0,1')) # BEIDOU
            gps_serial.close()

    def validate_gps(self, gps: GpsTelemetry, pos: tuple[float, float, str, str] | None) -> bool:
        if pos is None:
            return False

        delta = 0.0047
        (latitude, longitude, north_south, east_west) = pos
        if not (latitude - delta <= gps.latitude <= latitude + delta):
            return False

        if not (longitude - delta <= gps.longitude <= longitude + delta):
            return False

        if north_south != gps.north_south or east_west != gps.east_west:
            return False

        return True

    async def read_gps_position(self) -> tuple[float, float, str, str] | None:
        try:
            data: str = load_const('gps_position', str, None)
            if data is None:
                await self.SI_API.raise_alarm('UnfoundedGpsPosition', True, False)
                return None

            await self.SI_API.raise_alarm('UnfoundedGpsPosition', False, True)

            data = data.split(' ')
            latitude = float(data[0])
            longitude = float(data[1])
            north_south = data[2][0]
            east_west = data[2][1]

            await self.SI_API.raise_alarm('InvalidGpsPosition', False, True)
            return latitude, longitude, north_south, east_west
        except Exception:
            await self.SI_API.raise_alarm('InvalidGpsPosition', True, False)
            return None
